package main

import (
	"BilibiliConvertGo/upgrade"
	"BilibiliConvertGo/util"
	"embed"
	"fmt"
	"fyne.io/fyne/v2"
	"fyne.io/fyne/v2/app"
	"fyne.io/fyne/v2/canvas"
	"fyne.io/fyne/v2/container"
	"fyne.io/fyne/v2/dialog"
	"fyne.io/fyne/v2/layout"
	"fyne.io/fyne/v2/storage"
	"fyne.io/fyne/v2/widget"
	"github.com/go-resty/resty/v2"
	"github.com/google/uuid"
	"github.com/jageros/eventhub"
	"golang.org/x/sys/windows"
	"gopkg.in/eapache/queue.v1"
	"image/color"
	"io/ioutil"
	"log"
	"os"
	"os/exec"
	"path/filepath"
	"runtime"
	"strconv"
	"strings"
	"syscall"
	"time"
	"unsafe"
)

//go:embed assets
var assets embed.FS

//go:embed assets/simhei.ttf
var fontData []byte

const (
	sourcePathEvent       = 1
	targetPathEvent       = 2
	logAreaEvent          = 3
	progressUpdateEvent   = 4
	videoCntUpdateEvent   = 5
	successCntUpdateEvent = 6
)

const (
	logAreaEventCommandClear  = 1
	logAreaEventCommandAppend = 2
	logAreaEventCommandReset  = 3
)

const (
	progressCircleSize = 130
	logMaxLineCnt      = 30
	versionCode        = 11
)

const updateUrl = "http://bilibili.liang.hciot.com.cn/windows/version_" + runtime.GOOS + "_" + runtime.GOARCH
const openappUrl = "http://bilibili.liang.hciot.com.cn/openapp.php"
const transferUrl = "http://bilibili.liang.hciot.com.cn/transfer.php"

type Job struct {
	title           string
	sourcePath      string
	videoPath       string
	audioPath       string
	savePath        string
	part            string
	page            int
	fromPC          bool
	overrideOutput  bool
	sourceClearFlag bool
	onlyAudio       bool
	bitrate         int64
}

var (
	sourcePathInput      = &widget.Entry{}
	sourcePathInputValue = ""
	targetPathInput      = &widget.Entry{}
	targetPathInputValue = ""
	logAreaEntry         = &widget.Entry{}
	logAreaEntryValue    = ""
	circularBuffer       = &StringManipulator{}
	progressCircle       = &canvas.Circle{}
	progressLabelValue   = "0 %"
	progressLabel        = &widget.Label{}
	videoCntLabelValue   = "0"
	videoCntLabel        = &widget.Label{}
	successCntLabelValue = "0"
	successCntLabel      = &widget.Label{}
	mySoftwareSetting    = &softwareSetting{}
	jobs                 = queue.New()
	removeTempFileJob    = queue.New()
	jobStart             = false
	videoCnt             = 0
	successCnt           = 0
	skipCnt              = 0
	executeFlag          = false
)

// 程序启动
func main() {
	//设置中文字体:解决中文乱码问题
	initFont()
	eventInit()
	myapp := app.New()
	window := myapp.NewWindow(fmt.Sprintf("B站缓存转视频工具（版本号：%d）", versionCode))
	window.SetMaster()
	window.SetMainMenu(getMenuGroup(myapp, window))
	window.SetContent(container.NewVBox(getTopCtn(window), widget.NewSeparator(), getMainCtn(window)))
	window.Resize(fyne.NewSize(800, 440))

	// 软件设置初始化
	mySoftwareSetting = NewSoftwareSetting()
	clearTempDir()
	entryInit()
	logAreaInit()
	// 开启视频合并task
	go func() {
		for {
			executeJob()
			time.Sleep(500 * time.Millisecond)
		}
	}()
	// 开启视频转换解密文件清理task
	go func() {
		for {
			removeTempFileTask()
			time.Sleep(250 * time.Millisecond)
		}
	}()
	// 应用更新检测
	checkAndUpgrade(window)
	go util.HttpGetWithResult(openappUrl)
	window.ShowAndRun()
}

// 应用更新
func checkAndUpgrade(win fyne.Window) {
	go func() {
		upgrade.CheckVersion(updateUrl, func(res upgrade.VersionInfo) {
			if versionCode < res.VersionCode {
				confirmCnt := container.NewBorder(widget.NewLabel(fmt.Sprintf("检查到新版本：%s", res.Version)), nil, nil, nil)
				cdl := dialog.NewCustomConfirm("应用更新", "确认更新", "以后再说", confirmCnt, func(b bool) {
					if b {
						cmd := exec.Command("cmd", "/c", "start", "/b", "BilibiliConvertUpdate.exe", "-cmd", "upgrade", fmt.Sprintf("%d", versionCode))
						_ = cmd.Run()
					}
				}, win)
				cdl.Show()
			}
		}, func(err string) {
			d := dialog.NewInformation("应用更新", err, win)
			d.Show()
		})
	}()
}

// 清空缓存文件夹
func clearTempDir() {
	err := os.RemoveAll(".bilibili_convert_temp")
	if err != nil {
		eventhub.Publish(logAreaEvent, logAreaEventCommandAppend, fmt.Sprintf("缓存文件夹清理失败：%s", err))
		return
	}
	err = os.Mkdir(".bilibili_convert_temp", os.ModePerm)
	if err != nil {
		eventhub.Publish(logAreaEvent, logAreaEventCommandAppend, fmt.Sprintf("创建缓存文件夹失败：%s", err))
		return
	}
}

// 输入框初始化
func entryInit() {
	t, _ := filepath.Abs("")
	abs := storage.NewFileURI(t).Path()
	sourcePathInputValue = mySoftwareSetting.SourcePath
	if sourcePathInputValue == "" {
		sourcePathInputValue = abs
	}
	sourcePathInput.SetText(sourcePathInputValue)
	sourcePathInput.Refresh()

	targetPathInputValue = mySoftwareSetting.TargetPath
	if targetPathInputValue == "" {
		targetPathInputValue = abs
	}
	targetPathInput.SetText(targetPathInputValue)
	targetPathInput.Refresh()
}

// 日志框初始化
func logAreaInit() {
	// 尝试从网上加载初始化信息
	initStr := ""
	circularBuffer = NewStringManipulator("", logMaxLineCnt)
	loadFromAssets := false
	client := resty.New()
	resp, err := client.R().
		EnableTrace().
		Get("http://bilibili.liang.hciot.com.cn/windows/init.log")
	if err != nil {
		loadFromAssets = true
	} else {
		if resp.StatusCode() != 200 || len(resp.Body()) <= 0 {
			loadFromAssets = true
		}
	}

	// bytes转string
	if loadFromAssets {
		open, err := assets.ReadFile("assets/init.log")
		if err == nil {
			initStr = util.BytesToString(open)
		}
	} else {
		initStr = util.BytesToString(resp.Body())
	}

	// 重置日志输出框
	eventhub.Publish(logAreaEvent, logAreaEventCommandReset, initStr)
}

// 事件监听初始化
func eventInit() {
	eventhub.Subscribe(sourcePathEvent, func(args ...interface{}) {
		t, str := "", ""
		for _, arg := range args {
			//.(type)是判断interface{}的具体类型，只能在switch case语句中使用
			switch arg.(type) {
			case string:
				t = fmt.Sprintf("%v", arg)
			default:
				t = fmt.Sprintf("%v", arg)
			}
			str += t
		}
		str = strings.TrimSpace(str)
		index := strings.Index(str, "://")
		if index != -1 {
			str = str[index+3:]
		}
		if checkEntry(str) {
			sourcePathInputValue = str
			sourcePathInput.SetText(sourcePathInputValue)
			sourcePathInput.Refresh()
		}
	})
	eventhub.Subscribe(targetPathEvent, func(args ...interface{}) {
		t, str := "", ""
		for _, arg := range args {
			//.(type)是判断interface{}的具体类型，只能在switch case语句中使用
			switch arg.(type) {
			case string:
				t = fmt.Sprintf("%v", arg)
			default:
				t = fmt.Sprintf("%v", arg)
			}
			str += t
		}
		str = strings.TrimSpace(str)
		index := strings.Index(str, "://")
		if index != -1 {
			str = str[index+3:]
		}
		if checkEntry(str) {
			targetPathInputValue = str
			targetPathInput.SetText(targetPathInputValue)
		}
	})
	eventhub.Subscribe(logAreaEvent, func(args ...interface{}) {
		command := args[0]
		switch command {
		case 1:
			// 清空消息
			circularBuffer = NewStringManipulator("", logMaxLineCnt)
			logAreaEntryValue = ""
			break
		case 2:
			// 追加消息
			circularBuffer.AppendString(fmt.Sprintf("%v\n", args[1]))
			logAreaEntryValue = circularBuffer.GetString()
			break
		case 3:
			// 设置消息
			circularBuffer = NewStringManipulator("", logMaxLineCnt)
			circularBuffer.AppendString(fmt.Sprintf("%v\n", args[1]))
			logAreaEntryValue = circularBuffer.GetString()
			break
		default:
			break
		}
		logAreaEntry.SetText(logAreaEntryValue)
	})
	eventhub.Subscribe(progressUpdateEvent, func(args ...interface{}) {
		value := fmt.Sprintf("%v", args[0])
		v, _ := strconv.ParseFloat(value, 32)
		updateCircle(progressCircle, progressCircleSize, float32(v))
	})
	eventhub.Subscribe(videoCntUpdateEvent, func(args ...interface{}) {
		value := fmt.Sprintf("%v", args[0])
		videoCntLabelValue = value
		videoCntLabel.SetText(videoCntLabelValue)
		videoCntLabel.Refresh()
	})
	eventhub.Subscribe(successCntUpdateEvent, func(args ...interface{}) {
		value := fmt.Sprintf("%v", args[0])
		successCntLabelValue = value
		successCntLabel.SetText(successCntLabelValue)
		successCntLabel.Refresh()
	})
}

// 检测选择文件夹是否合法，若不存在，则创建
func checkEntry(input string) bool {
	stat, err := os.Stat(input)
	if err != nil {
		if os.IsNotExist(err) {
			eventhub.Publish(logAreaEvent, logAreaEventCommandAppend, fmt.Sprintf("创建文件夹：%s", input))
			err := os.MkdirAll(input, os.ModePerm)
			if err != nil {
				eventhub.Publish(logAreaEvent, logAreaEventCommandAppend, fmt.Sprintf("无效的文件夹：%s", input))
				return false
			}
			stat, err = os.Stat(input)
		}
	}
	if !stat.IsDir() {
		eventhub.Publish(logAreaEvent, logAreaEventCommandAppend, fmt.Sprintf("您选择的并不是一个目录：%s", input))
		return false
	}
	return true
}

// 字体初始化
func initFont() {
	temp, err := ioutil.TempDir("", "font")
	if err != nil {
		return
	}
	tempPath := filepath.Join(temp, "simhe.ttf")
	_ = os.WriteFile(tempPath, fontData, 0755)
	_ = os.Setenv("FYNE_FONT", tempPath)
}

// 更新圆形的尺寸
// param1: 需要变换的circle
// param2: circle为100%时的尺寸
// param3: 当前显示的百分比
func updateCircle(obj fyne.CanvasObject, max float32, percentage float32) {
	value := max * (percentage / 100)
	obj.Resize(fyne.NewSize(value, value))
	pos := (max - value) / 2
	obj.Move(fyne.NewPos(pos, pos))
	canvas.Refresh(obj)

	// 更新进度文字显示
	progressLabelValue = fmt.Sprintf("%.2f", percentage) + " %"
	progressLabel.SetText(progressLabelValue)
	progressLabel.Refresh()
}

func getTopCtn(win fyne.Window) *fyne.Container {
	// 输入框
	sourceLabel := widget.NewLabel("缓存路径")
	sourcePathInput = widget.NewEntry()
	sourceBtn := widget.NewButton("打开", func() {
		folderOpen := dialog.NewFolderOpen(func(list fyne.ListableURI, err error) {
			if err != nil {
				dialog.ShowError(err, win)
				return
			}
			if list == nil {
				log.Println("Cancelled")
				return
			}
			eventhub.Publish(sourcePathEvent, list.Path())
		}, win)
		folderOpen.Resize(fyne.NewSize(800, 800))
		testData, _ := filepath.Abs(sourcePathInputValue)
		dir, err := storage.ListerForURI(storage.NewFileURI(testData))
		if err != nil {
			fmt.Println(err)
		}
		folderOpen.SetLocation(dir)
		folderOpen.Show()
	})
	sourceGroup := container.NewBorder(layout.NewSpacer(), layout.NewSpacer(), sourceLabel, sourceBtn, sourcePathInput)

	targetLabel := widget.NewLabel("保存路径")
	targetPathInput = widget.NewEntry()
	targetBtn := widget.NewButton("打开", func() {
		folderOpen := dialog.NewFolderOpen(func(list fyne.ListableURI, err error) {
			if err != nil {
				dialog.ShowError(err, win)
				return
			}
			if list == nil {
				log.Println("Cancelled")
				return
			}
			eventhub.Publish(targetPathEvent, list.Path())
		}, win)
		folderOpen.Resize(fyne.NewSize(800, 800))
		testData, _ := filepath.Abs(targetPathInputValue)
		dir, err := storage.ListerForURI(storage.NewFileURI(testData))
		if err != nil {
			fmt.Println(err)
		}
		folderOpen.SetLocation(dir)
		folderOpen.Show()
	})
	targetGroup := container.NewBorder(layout.NewSpacer(), layout.NewSpacer(), targetLabel, targetBtn, targetPathInput)
	return container.NewVBox(sourceGroup, targetGroup)
}

// 检测文件夹是否合法
func checkEntryInput() bool {
	// 检测源文件路径
	if checkEntry(sourcePathInput.Text) {
		sourcePathInputValue = sourcePathInput.Text
		sourcePathInput.Refresh()
	} else {
		return false
	}
	// 检测目标文件路径
	if checkEntry(targetPathInput.Text) {
		targetPathInputValue = targetPathInput.Text
		targetPathInput.Refresh()
	} else {
		return false
	}
	return true
}

// 获得指定盘符的剩余空间大小，单位字节
func getDiskFreeSpace(path string) (int64, error) {
	if strings.Index(path, ":") == -1 {
		return 0, nil
	}
	h := syscall.MustLoadDLL("kernel32.dll")
	c := h.MustFindProc("GetDiskFreeSpaceExW")
	lpFreeBytesAvailable := int64(0)
	lpTotalNumberOfBytes := int64(0)
	lpTotalNumberOfFreeBytes := int64(0)
	_, _, err := c.Call(uintptr(unsafe.Pointer(syscall.StringToUTF16Ptr(path))),
		uintptr(unsafe.Pointer(&lpFreeBytesAvailable)),
		uintptr(unsafe.Pointer(&lpTotalNumberOfBytes)),
		uintptr(unsafe.Pointer(&lpTotalNumberOfFreeBytes)))
	return lpFreeBytesAvailable, err
}

// 获取文件夹的大小，单位字节
func getDirectorySize(path string) (int64, error) {
	var size int64
	err := filepath.Walk(path, func(filePath string, info os.FileInfo, err error) error {
		if err != nil {
			return err
		}
		if !info.IsDir() {
			size += info.Size()
		}
		return nil
	})
	if err != nil {
		return 0, err
	}
	return size, nil
}

// 格式化字节大小
func formatByteSize(size int64) string {
	const (
		_  = iota
		KB = 1 << (10 * iota)
		MB
		GB
	)
	switch {
	case size >= GB:
		return fmt.Sprintf("%0.2f G", float64(size)/GB)
	case size >= MB:
		return fmt.Sprintf("%0.2f MB", float64(size)/MB)
	case size >= KB:
		return fmt.Sprintf("%0.2f K", float64(size)/KB)
	default:
		return fmt.Sprintf("%d B", size)
	}
}

func getMainCtn(win fyne.Window) *fyne.Container {
	// 转换按钮
	transferBtn := widget.NewButton("转换", func() {
		if checkFFmpeg() {
			if !checkEntryInput() {
				return
			}
			eventhub.Publish(logAreaEvent, logAreaEventCommandAppend, fmt.Sprintf("源文件路径：%s", sourcePathInputValue))
			eventhub.Publish(logAreaEvent, logAreaEventCommandAppend, fmt.Sprintf("目标路径：%s", targetPathInputValue))
			// 统计清零
			eventhub.Publish(videoCntUpdateEvent, 0)
			eventhub.Publish(successCntUpdateEvent, 0)

			// 记录点击次数
			go util.HttpGetWithResult(transferUrl)

			// 输出盘符剩余空间大小
			freeSpace, err := getDiskFreeSpace(targetPathInputValue)
			if err != nil && err != windows.ERROR_SUCCESS {
				errMsg := fmt.Sprintf("【错误】无法判断保存文件夹剩余大小：%s", err.Error())
				eventhub.Publish(logAreaEvent, logAreaEventCommandAppend, errMsg+"\r\n")
				dialog.NewInformation("报错啦!!!∑(ﾟДﾟノ)ノ", errMsg, win).Show()
				return
			}
			// 源视频目录的大小
			size, err := getDirectorySize(sourcePathInputValue)
			if err != nil {
				errMsg := fmt.Sprintf("【错误】无法判断源文件夹视频大小：%s", err.Error())
				eventhub.Publish(logAreaEvent, logAreaEventCommandAppend, errMsg+"\r\n")
				dialog.NewInformation("报错啦!!!∑(ﾟДﾟノ)ノ", errMsg, win).Show()
				return
			}
			eventhub.Publish(logAreaEvent, logAreaEventCommandAppend, fmt.Sprintf("源视频目录大小：%s; 目标盘符剩余空间：%s", formatByteSize(size), formatByteSize(freeSpace)))
			if freeSpace < (size + 1024*1024*1024*mySoftwareSetting.DiskProtect) {
				errMsg := fmt.Sprintf("【失败】所选的保存路径空间太小了，设定的磁盘保护值为【%v】G\r\n转换后剩余[%s]，建议您换一个盘符（例如D盘，E盘）", mySoftwareSetting.DiskProtect, formatByteSize(freeSpace-size))
				eventhub.Publish(logAreaEvent, logAreaEventCommandAppend, errMsg+"\r\n")
				dialog.NewInformation("报错啦!!!∑(ﾟДﾟノ)ノ", errMsg, win).Show()
				return
			}
			currentAbsPath, err := filepath.Abs("")
			if err != nil {
				errMsg := "无法判断当前文件夹信息，请重试"
				eventhub.Publish(logAreaEvent, logAreaEventCommandAppend, errMsg+"\r\n")
				dialog.NewInformation("报错啦!!!∑(ﾟДﾟノ)ノ", errMsg, win).Show()
				return
			} else {
				currentDiskFreeSpace, err := getDiskFreeSpace(currentAbsPath)
				if err != nil && err != windows.ERROR_SUCCESS {
					errMsg := fmt.Sprintf("【错误】无法判断当前盘符大小：%s", err.Error())
					eventhub.Publish(logAreaEvent, logAreaEventCommandAppend, errMsg+"\r\n")
					dialog.NewInformation("报错啦!!!∑(ﾟДﾟノ)ノ", errMsg, win).Show()
					return
				} else {
					if currentDiskFreeSpace <= (size + 1024*1024*1024*mySoftwareSetting.DiskProtect) {
						errMsg := fmt.Sprintf("当前盘符太小了，请把软件移动到其他盘符再运行\r\n设定的磁盘保护值为【%v】G，但当前仅剩【%s】", mySoftwareSetting.DiskProtect, formatByteSize(currentDiskFreeSpace))
						eventhub.Publish(logAreaEvent, logAreaEventCommandAppend, errMsg+"\r\n")
						dialog.NewInformation("报错啦!!!∑(ﾟДﾟノ)ノ", errMsg, win).Show()
						return
					}
				}
			}

			// 解析视频
			videoInfos := getVideoInfos(sourcePathInputValue)
			// 更新视频个数
			videoCnt = len(videoInfos)
			eventhub.Publish(videoCntUpdateEvent, videoCnt)
			eventhub.Publish(logAreaEvent, logAreaEventCommandAppend, fmt.Sprintf("找到视频个数：%d", videoCnt))

			successCnt = 0
			skipCnt = 0
			for i := range videoInfos {
				videoInfo := videoInfos[i]
				if util.IsAnyBlank(videoInfo.title, videoInfo.part, videoInfo.videoPath, videoInfo.audioPath) {
					continue
				} else {
					// 判断是否设置不覆盖保存，且视频已存在
					// 目标视频的所在目录
					outputVideoSavePath := targetPathInputValue + string(filepath.Separator) + videoInfo.title
					// 创建目录
					err := os.MkdirAll(outputVideoSavePath, os.ModePerm)
					if err != nil {
						eventhub.Publish(logAreaEvent, logAreaEventCommandAppend, fmt.Sprintf("无法创建文件夹：%s"+outputVideoSavePath))
						eventhub.Publish(logAreaEvent, logAreaEventCommandAppend, "已跳过该任务")
						continue
					}
					// 拼接视频的名称
					suffix := "mp4"
					if mySoftwareSetting.OutputFormat != "" {
						suffix = mySoftwareSetting.OutputFormat
					}
					if mySoftwareSetting.OnlyAudio {
						suffix = "mp3"
						if mySoftwareSetting.OutputAudioFormat != "" {
							suffix = mySoftwareSetting.OutputAudioFormat
						}
					}

					if mySoftwareSetting.OutputNameFormat != "" {
						tName := mySoftwareSetting.OutputNameFormat
						tName = strings.ReplaceAll(tName, "$[page]", fmt.Sprintf("%d", videoInfo.page))
						tName = strings.ReplaceAll(tName, "$[part]", videoInfo.part)
						tName = strings.ReplaceAll(tName, "$[title]", videoInfo.title)
						outputVideoSavePath = outputVideoSavePath + string(filepath.Separator) + tName + "." + suffix
					} else {
						outputVideoSavePath = outputVideoSavePath + string(filepath.Separator) + strconv.Itoa(videoInfo.page) + "-" + videoInfo.part + "." + suffix
					}

					// 如果设置了，不覆盖转换则检测文件是否已经存在
					if !mySoftwareSetting.OverrideOutput {
						_, err = os.Stat(outputVideoSavePath)
						if err == nil {
							eventhub.Publish(logAreaEvent, logAreaEventCommandAppend, fmt.Sprintf("视频[%s: %d、%s]已存在，不处理", videoInfo.title, videoInfo.page, videoInfo.part))
							skipCnt++
							continue
						}
					}

					// 如果是电脑端的缓存，需要先解密
					if videoInfo.fromPC {
						abs, err := filepath.Abs("")
						if err != nil {
							continue
						}
						tempPath := abs + string(filepath.Separator) + ".bilibili_convert_temp" + string(filepath.Separator) + uuid.New().String() + ".m4s"
						// 如果仅仅处理音频，就不需要解析视频文件
						if !mySoftwareSetting.OnlyAudio {
							eventhub.Publish(logAreaEvent, logAreaEventCommandAppend, fmt.Sprintf("==>开始解密视频[%s: %s、%s]", videoInfo.title, videoInfo.part, videoInfo.videoPath))
							// 更新进度
							eventhub.Publish(progressUpdateEvent, 0)
							if util.DecodeM4s(videoInfo.videoPath, tempPath, func(p float32) {
								if p < 0 {
									eventhub.Publish(logAreaEvent, logAreaEventCommandAppend, fmt.Sprintf("==>视频[%s: %s、%s]解密失败！", videoInfo.title, videoInfo.part, videoInfo.videoPath))
								} else if p >= 100 {
									eventhub.Publish(logAreaEvent, logAreaEventCommandAppend, fmt.Sprintf("==>视频[%s: %s、%s]解密完成", videoInfo.title, videoInfo.part, videoInfo.videoPath))
								} else {
									eventhub.Publish(progressUpdateEvent, p)
								}
							}) {
								videoInfo.videoPath = tempPath
							} else {
								continue
							}
						}

						tempPath = abs + string(filepath.Separator) + ".bilibili_convert_temp" + string(filepath.Separator) + uuid.New().String() + ".m4s"
						eventhub.Publish(logAreaEvent, logAreaEventCommandAppend, fmt.Sprintf("==>开始解密视频[%s: %s、%s]", videoInfo.title, videoInfo.part, videoInfo.audioPath))
						// 更新进度
						eventhub.Publish(progressUpdateEvent, 0)
						if util.DecodeM4s(videoInfo.audioPath, tempPath, func(p float32) {
							if p < 0 {
								eventhub.Publish(logAreaEvent, logAreaEventCommandAppend, fmt.Sprintf("==>视频[%d、%s=>%s]解密失败！", videoInfo.page, videoInfo.part, videoInfo.audioPath))
							} else if p == 100 {
								eventhub.Publish(logAreaEvent, logAreaEventCommandAppend, fmt.Sprintf("==>视频[%d、%s=>%s]解密完成", videoInfo.page, videoInfo.part, videoInfo.audioPath))
							} else {
								eventhub.Publish(progressUpdateEvent, p)
							}
						}) {
							videoInfo.audioPath = tempPath
						} else {
							continue
						}
					}
					// 添加任务
					jobs.Add(Job{
						title:           videoInfo.title,
						sourcePath:      videoInfo.sourcePath,
						videoPath:       videoInfo.videoPath,
						audioPath:       videoInfo.audioPath,
						part:            videoInfo.part,
						page:            videoInfo.page,
						fromPC:          videoInfo.fromPC,
						savePath:        outputVideoSavePath,
						overrideOutput:  mySoftwareSetting.OverrideOutput,
						sourceClearFlag: mySoftwareSetting.SourceClearFlag,
						onlyAudio:       mySoftwareSetting.OnlyAudio,
						bitrate:         mySoftwareSetting.Bitrate,
					})
				}
			}
			jobStart = true
			executeFlag = true
		} else {
			msg := "无法检测ffmpeg环境，请重新下载该软件或者更新后重试"
			eventhub.Publish(logAreaEvent, logAreaEventCommandAppend, msg)
		}
	})

	// 清除日志
	clearLogBtn := widget.NewButton("清空日志", func() {
		eventhub.Publish(logAreaEvent, logAreaEventCommandClear)
	})

	// 进度条
	backGroundCircle := canvas.NewCircle(color.NRGBA{R: 251, G: 251, B: 251, A: 255})
	backGroundCircle.StrokeColor = color.Gray{0x99}
	backGroundCircle.StrokeWidth = 2
	backGroundCircle.Resize(fyne.NewSize(progressCircleSize, progressCircleSize))
	canvas.Refresh(backGroundCircle)
	progressCircle = canvas.NewCircle(color.NRGBA{R: 207, G: 255, B: 134, A: 255})
	progress := container.NewWithoutLayout(backGroundCircle, progressCircle)

	// label
	progressLabel = widget.NewLabel(progressLabelValue)
	progressLabelBorder := container.NewBorder(layout.NewSpacer(), nil, widget.NewLabel("进度："), progressLabel)
	videoCntLabel = widget.NewLabel(videoCntLabelValue)
	videoCntLabelBorder := container.NewBorder(nil, nil, widget.NewLabel("视频数："), videoCntLabel)
	successCntLabel = widget.NewLabel(successCntLabelValue)
	successCntLabelBorder := container.NewBorder(nil, nil, widget.NewLabel("转换完成数："), successCntLabel)

	// 赞助
	file, _ := assets.ReadFile("assets/qrCode.png")
	qrCode := canvas.NewImageFromResource(fyne.NewStaticResource("qrCode.png", file))
	qrCode.Resize(fyne.NewSize(progressCircleSize, progressCircleSize))
	qrCode.Refresh()
	sponsor := container.NewGridWrap(fyne.NewSize(progressCircleSize, progressCircleSize), qrCode)
	control := container.NewVBox(
		transferBtn,
		clearLogBtn,
		progress,
		widget.NewLabel("                  "),
		widget.NewLabel("                  "),
		widget.NewLabel("                  "),
		progressLabelBorder,
		videoCntLabelBorder,
		successCntLabelBorder,
		sponsor,
		widget.NewLabel("↗↑疯狂暗示↑↑↖"))
	logAreaEntry = widget.NewMultiLineEntry()
	return container.NewBorder(layout.NewSpacer(), layout.NewSpacer(), control, nil, logAreaEntry)
}

// 菜单组
func getMenuGroup(myApp fyne.App, myWindow fyne.Window) *fyne.MainMenu {
	settingMenuItem := fyne.NewMenuItem("软件设置", func() {
		myWindow := myApp.NewWindow("软件设置")
		myWindow.SetContent(mySoftwareSetting.GetMenu(myWindow))
		myWindow.Resize(fyne.NewSize(600, 250))
		myWindow.Show()
	})
	themeMenuItem := fyne.NewMenuItem("主题设置", func() {
		w := myApp.NewWindow("主题设置")
		w.SetContent(NewSettings().LoadAppearanceScreen(w))
		w.Resize(fyne.NewSize(480, 480))
		w.Show()
	})
	checkUpgradeMenu := fyne.NewMenuItem("检测更新", func() {
		if !isProcessRunning("BilibiliConvertUpdate.exe") {
			cmd := exec.Command("cmd", "/c", "start", "/b", "BilibiliConvertUpdate.exe", "-cmd", "upgrade", fmt.Sprintf("%d", versionCode))
			cmd.SysProcAttr = &syscall.SysProcAttr{HideWindow: true}
			_ = cmd.Run()
		}
	})
	restartAndUpgradeMenu := fyne.NewMenuItem("重启并更新", func() {
		if !isProcessRunning("BilibiliConvertUpdate.exe") {
			cmd := exec.Command("cmd", "/c", "start", "/b", "BilibiliConvertUpdate.exe", "-cmd", "apply", fmt.Sprintf("%d", versionCode))
			cmd.SysProcAttr = &syscall.SysProcAttr{HideWindow: true}
			_ = cmd.Run()
		}
	})

	return fyne.NewMainMenu(
		// a quit item will be appended to our first menu
		fyne.NewMenu("菜单", settingMenuItem, fyne.NewMenuItemSeparator(), themeMenuItem),
		fyne.NewMenu("更新", checkUpgradeMenu, fyne.NewMenuItemSeparator(), restartAndUpgradeMenu),
	)
}

// 检测进程是否在运行
func isProcessRunning(processName string) bool {
	cmd := exec.Command("tasklist")
	output, err := cmd.Output()
	if err != nil {
		log.Fatal(err)
	}
	return strings.Contains(string(output), processName)
}

// 调用ffmpeg进行视频合并的task
func executeJob() {
	if jobStart && jobs.Length() > 0 {
		job := jobs.Remove()
		videoInfo := job.(Job)
		// 更新进度
		eventhub.Publish(progressUpdateEvent, 0)
		mergeM4S(videoInfo.videoPath, videoInfo.audioPath, videoInfo.savePath, videoInfo.overrideOutput, videoInfo.onlyAudio, videoInfo.bitrate, func(p float32) {
			fmt.Println(p)
			eventhub.Publish(progressUpdateEvent, p)
			if p == 100 {
				eventhub.Publish(logAreaEvent, logAreaEventCommandAppend, fmt.Sprintf("视频[%s: %d、%s]转换完成", videoInfo.title, videoInfo.page, videoInfo.part))
				// 更新转换成功个数
				successCnt++
				eventhub.Publish(successCntUpdateEvent, successCnt)
				if videoInfo.fromPC {
					if !videoInfo.onlyAudio {
						removeTempFileJob.Add(videoInfo.videoPath)
					}
					removeTempFileJob.Add(videoInfo.audioPath)
				}
				if videoInfo.sourceClearFlag && videoInfo.sourcePath != "" {
					removeTempFileJob.Add(videoInfo.sourcePath)
				}
			}
		})
	} else if executeFlag && jobs.Length() == 0 {
		eventhub.Publish(logAreaEvent, logAreaEventCommandAppend, "==========视频转换完成==========")
		eventhub.Publish(logAreaEvent, logAreaEventCommandAppend, fmt.Sprintf("|| 视频数：%d\t\t\t\t||", videoCnt))
		eventhub.Publish(logAreaEvent, logAreaEventCommandAppend, fmt.Sprintf("|| 成功数：%d\t\t\t\t||", successCnt))
		eventhub.Publish(logAreaEvent, logAreaEventCommandAppend, fmt.Sprintf("|| 跳过数：%d\t\t\t\t||", skipCnt))
		eventhub.Publish(logAreaEvent, logAreaEventCommandAppend, fmt.Sprintf("|| 转换完成度：%.2f%%\t\t||", float32(successCnt+skipCnt)/float32(videoCnt)*float32(100)))
		eventhub.Publish(logAreaEvent, logAreaEventCommandAppend, "================================")
		jobStart = false
		executeFlag = false
	}
}

// 删除解密文件的task
func removeTempFileTask() {
	if removeTempFileJob.Length() > 0 {
		path := removeTempFileJob.Remove()
		filePath := path.(string)
		_ = os.RemoveAll(filePath)
	}
}
